package sk.ygor.raynor.rebind.serializable

import com.google.gwt.core.ext.typeinfo._
import scala.collection.mutable
import sk.ygor.raynor.client.json.{JSONProperty, JSONSerializationException}
import sk.ygor.raynor.client.json.JSONSerializer.ParametrizedClassName
import sk.ygor.raynor.rebind.common.GeneratorUtils

object GenericTypeAnalysis extends TypeAnalysis {
  def runAnalysis(serializableType: JType): AnalysisResult = {

    try {
      isReady(serializableType) match {
        case Ready(jClassType) =>
          Analyzed(GenericTypeStructure(jClassType, Right(analyzeFields(jClassType)), jClassType.getSubtypes.toList))
        case NotReady(reason, jClassType) =>
          Analyzed(GenericTypeStructure(jClassType, Left(reason), jClassType.getSubtypes.toList))
        case Invalid(reason) =>
          Rejected(reason)
      }
    } catch {
      case e => return Rejected(e.getMessage)
    }
  }

  abstract class IsReady
  case class Ready(jClassType: JClassType) extends IsReady
  case class NotReady(reason: String, jClassType: JClassType) extends IsReady
  case class Invalid(reason: String) extends IsReady

  def isReady(jType: JType): IsReady = {
    jType.maybeClass match {
      case Some(classType) => if (classType.isAbstract) {
        NotReady("abstract class", classType)
      } else if (!hasNoArgConstructor(classType)) {
        NotReady("class without no-arg constructor", classType)
      } else {
        classType.maybeRawType match {
          case Some(rawType) => Invalid("a raw type -  that is a generic type with no type arguments")
          case None => Ready(classType)
        }

      }
      case None => jType.maybeInterface match {
        case Some(interfaceType) => NotReady("interface", interfaceType)
        case None => Invalid("not a class or interface")
      }
    }
  }


  def hasNoArgConstructor(jClassType: JClassType) = jClassType.getConstructors.exists(c => c.getParameters.isEmpty)

  def analyzeFields(serializableType: JClassType): Map[GenericTypeStructureProperty, JType] = {
    val properties = mutable.Map.empty[GenericTypeStructureProperty, JType]
    for (jField: JField <- serializableType.getFields()) {
      val property = jField.findAnnotation(classOf[JSONProperty]) match {
        case Some(annotation) => GenericTypeStructureProperty(jField.getName, annotation.jsonPropertyName)
        case None => GenericTypeStructureProperty(jField.getName, jField.getName)
      }
      if (properties.keys.map(_.jsonFieldName) contains(property.jsonFieldName)) {
        throw new JSONSerializationException("JSON property '" + property.jsonFieldName + "'" +
                " defined more than once");
      }
      properties += (property -> jField.getType());
    }
    Map() ++ properties
  }

}

case class GenericTypeStructureProperty(javaFieldName: String, jsonFieldName: String)

case class GenericTypeStructure(asClassType: JClassType,
                                properties: Either[String, Map[GenericTypeStructureProperty, JType]],
                                subTypes: List[JType]) extends StructureInfo with GeneratorUtils {


  private val pkg = asClassType.getPackage.getName

  val fullName = asClassType.getParameterizedQualifiedSourceName
  val blankName = asClassType.getQualifiedSourceName

  private def fullSimpleName = fullName.substring(pkg.length + 1).replaceAll("\\.", "_").replaceAll("[<>,]", "__")

  def parametrizedClassName = {
    def parametrizedClassName(c: JClassType): ParametrizedClassName =
      new ParametrizedClassName(
        c.getPackage.getName,
        c.getQualifiedSourceName.substring(c.getPackage.getName.length + 1),
        c.maybeParameterized match {
          case Some(parametrized) => parametrized.getTypeArgs.map(parametrizedClassName(_)).toArray
          case None => Array[ParametrizedClassName]()
        })
    parametrizedClassName(asClassType)
  }


  def serializer = ClassInfo(pkg, fullSimpleName + "_JSONSerializer")

  def dependendTypes = subTypes ++ (properties match {
    case Right(properties) => List.fromIterator(properties.values)
    case Left(reason) => List()
  })
}







